package com.smithcarlson.knucleotide

/*
   The Computer Language Benchmarks Game
   http://shootout.alioth.debian.org/
   Based partially on the single-threaded version by Isaac Gouy with fixes from
   Iulian Dragos and Meiko Rachimow.
   Based on Scala contribution of Rex Kerr
   Based on bit encoding idea of C++ contribution of Andrew Moon
   Contributed by The Anh Tran
   Updated for 2.8 by Rex Kerr
*/

import scala.actors.Futures.future
import scala.actors.Future
import scala.collection.mutable.HashMap
import java.io._
import scala.collection.IndexedSeqLike
import scala.collection.IndexedSeqOptimized
import scala.annotation.tailrec

final object knucleotide {
  def main(args: Array[String]) = {
    val startTime: Long = System.currentTimeMillis();
    // read all data from inputstream
    val data = Helper.readAll
    val block = Block(data, 0, data.length, data.length)

    // hand out tasks, each task will be picked up by scala threadpool
    val tables = List(1, 2, 3, 4, 6, 12, 18)
      //      val tables = List(1, 2, 12, 18)
      .map(sz => queueBuildHashtable(block, sz))
      .splitAt(2)

    // print frequency of each nucleotide
    tables._1.foreach(printFreq(_, data.length))

    // print how many times a specific pattern appears in input data
    val codeSeq = List("GGT", "GGTA", "GGTATT", "GGTATTTTAATT", "GGTATTTTAATTTATAGT");
    //      val codeSeq = List("GGTATTTTAATT", "GGTATTTTAATTTATAGT");
    (tables._2 zip codeSeq).foreach(e => printFreq(e._1, e._2))
    val endTime: Long = System.currentTimeMillis();

    println("Time: " + (endTime - startTime));
  }

  private def printFreq(lht: List[Future[KnuHashMap]], data_length: Int) = {
    // have to merge (whole list of results) into a hashmap
    val ht = mergeTables(lht)

    // sort by decending frequencies
    val sorted_list = ht.toArray.sortWith((a, b) => !(a._2 < b._2))

    val total = data_length.toFloat

    sorted_list foreach (a => printf("%s %.3f\n",
      a._1.decode,
      (a._2.toFloat * 100.0f / total)))

    println
  }

  private def printFreq(lht: List[Future[KnuHashMap]], pt: String) = {
    val ht = mergeTables(lht)

    val k = NucleicAcidFragment(pt.toCharArray, 0, pt.length)
    assert(pt == k.decode)

    val v = ht.getOrElse(k, 0)
    printf("%d\t%s\n", v, pt)
  }

  private def queueBuildHashtable(data: Block[Char], frameSize: Int) = {
    // each task will hash whole input data, at a specific offset
    // with (0 <= offset < frameSize)
    (0 until frameSize)
      .map(offset => future(buildHashtable(data, frameSize, offset)))
      .toList
  }

  private def buildHashtable(data: Block[Char], frameSize: Int, startOffset: Int): KnuHashMap = {
    val ht = new KnuHashMap

    val i_end = data.length - frameSize + 1
    var i_beg = startOffset

    while (i_beg < i_end) {
      val key = NucleicAcidFragment(data, i_beg, frameSize)
      ht.incCounter(key)

      i_beg += frameSize
    }

    ht
  }

  private def mergeTables(list_hashtable: List[Future[KnuHashMap]]) =
    list_hashtable
      .map(_())
      .reduceLeft((t1, t2) => {
        t2.foreach(e => t1.addCounter(e._1, e._2))
        t1
      })
}

final class NucleicAcidFragment(val encoding: Long, val length: Int) {
  def this(hlen: Int) = this(0, hlen)

  @inline
  override def clone() = new NucleicAcidFragment(encoding, length)

  @inline
  override def hashCode(): Int = encoding.toInt

  @inline
  override def equals(other: Any) = other match {
    case that: NucleicAcidFragment => this.encoding == that.encoding &&
      this.length == that.length
    case _ => false
  }

  def decode() = {
    val sb = new StringBuilder(length)
    var index = 0
    var extract_mask = NucleicAcidFragment.bit_mask
    while (index < length) {
      val extract_value = ((encoding & extract_mask) >>> (index * NucleicAcidFragment.bit_per_code)).toInt
      sb append (IubCode(extract_value))
      extract_mask <<= NucleicAcidFragment.bit_per_code
      index += 1
    }
    sb toString
  }
}

object NucleicAcidFragment {
  val bit_mask = 3L
  val bit_per_code = 2

  @inline
  def apply(data: IndexedSeq[Char], offset: Int, length: Int): NucleicAcidFragment = {
    require(length <= 31, "Nucleic Acid Fragment overflow, max 31 IUB codes")
    var encoding: Long = 0L
    var index = 0
    var shift = 0
    while (index < length) {
      encoding |= (IubCode(data(offset + index)) << shift)
      shift += bit_per_code
      index += 1
    }
    new NucleicAcidFragment(encoding, length)
  }

}

private final object IubCode {

  @inline
  def apply(c: Char): Long = (c: @annotation.switch) match {
    case 'a' => 0
    case 't' => 1
    case 'c' => 2
    case 'g' => 3

    case 'A' => 0
    case 'T' => 1
    case 'C' => 2
    case 'G' => 3

    case _ => assert(false); -1
  }

  private val Int2Iub = Array('A', 'T', 'C', 'G')

  @inline
  def apply(c: Int): Char = Int2Iub(c)

}

private final class KnuHashMap extends HashMap[NucleicAcidFragment, Int] {
  @inline
  def incCounter(key: NucleicAcidFragment): Unit = addCounter(key, 1)

  @inline
  def addCounter(key: NucleicAcidFragment, valToAdd: Int) {
    // directly look up entry inside hashtable
    var e = table(index(key.hashCode)).asInstanceOf[Entry]
    while (e != null) {
      if (e.key == key) { e.value += valToAdd; return }
      else e = e.next
    }
    addEntry(new Entry(key.clone, valToAdd))
  }
}

private final object Helper {
  def readAll() = {
    val sourceFile = new File("knucleotide_2_5E5.fasta")
    val reader = new BufferedReader(new FileReader(sourceFile));
    // val reader = new BufferedReader(new InputStreamReader(System.in, "US-ASCII"), 4 * 1024 * 1024)

    var line = reader readLine ()
    while ((line != null) && ((line(0) != '>') || (line.startsWith(">THREE") == false)))
      line = reader readLine

    val sb = new StringBuilder(32 * 1024 * 1024)

    line = reader readLine ()
    while (line != null) {
      sb append line
      line = reader readLine
    }

    // Read a char by "String.charAt(index)" is much slower than "Array(index)"
    // => use Array[Char]
    sb.toString.toCharArray
  }
}

/// Switch around and take the tail from the previous block... Duh.
class Block {
  def buffer: Array[Char]
  def maxStartIndex: Int
  def bufferLength: Int
  def produce(offset: Int, length: Int): NucleicAcidFragment

  def getStream(length: Int): Stream[NucleicAcidFragment] = {
    return _getStream(0, length);
  }

  private final def _getStream(offset: Int, length: Int): Stream[NucleicAcidFragment] = {
    if (offset <= maxStartIndex || offset + length > bufferLength) {
      Stream.Empty
    } else {
      Stream.cons(NucleicAcidFragment(buffer, offset, length), _getStream(offset + 1, length))
    }
  }

  def tail(maxLength: Int): Array[Char] = {
    nextBlock match {
      case Some(block) if buffer.length < maxLength =>
        val nextBlockHead = block.head(maxLength - buffer.length)
        val head = new Array[Char](Math.min(maxLength, buffer.length + nextBlockHead.length))
        buffer.copyToArray(head)
        nextBlockHead.copyToArray(head, buffer.length, nextBlockHead.length)
        head
      case _ =>
        val head: Array[Char] = new Array[Char](Math.min(maxLength, buffer.length))
        buffer.copyToArray(head, 0, maxLength)
        head
    }
  }
}

object Block {
  def apply(head: Array[Char], body: Array[Char]): Block = {

  }
}
